package paulscode.sound.libraries;

import java.util.LinkedList;
import javax.sound.sampled.AudioFormat;

import paulscode.sound.Channel;
import paulscode.sound.FilenameURL;
import paulscode.sound.ListenerData;
import paulscode.sound.SoundBuffer;
import paulscode.sound.Source;
import paulscode.sound.SoundSystemConfig;
import paulscode.sound.Vector3D;

/**
 * The SourceJavaSound class provides an interface to the JavaSound library. For
 * more information about the Java Sound API, please visit
 * http://java.sun.com/products/java-media/sound/ <br>
 * <br>
 * <b><i> SoundSystem LibraryJavaSound License:</b></i><br>
 * <b><br>
 * You are free to use this library for any purpose, commercial or otherwise.
 * You may modify this library or source code, and distribute it any way you
 * like, provided the following conditions are met: <br>
 * 1) You may not falsely claim to be the author of this library or any
 * unmodified portion of it. <br>
 * 2) You may not copyright this library or a modified version of it and then
 * sue me for copyright infringement. <br>
 * 3) If you modify the source code, you must clearly document the changes made
 * before redistributing the modified source code, so other users know it is not
 * the original code. <br>
 * 4) You are not required to give me credit for this library in any derived
 * work, but if you do, you must also mention my website:
 * http://www.paulscode.com <br>
 * 5) I the author will not be responsible for any damages (physical, financial,
 * or otherwise) caused by the use if this library or any part of it. <br>
 * 6) I the author do not guarantee, warrant, or make any representations,
 * either expressed or implied, regarding the use of this library or any part of
 * it. <br>
 * <br>
 * Author: Paul Lamb <br>
 * http://www.paulscode.com </b>
 */
public class SourceJavaSound extends Source {
	/**
	 * The source's basic Channel type-cast to a ChannelJavaSound.
	 */
	protected ChannelJavaSound channelJavaSound = (ChannelJavaSound) channel;

	/**
	 * Handle to the listener information.
	 */
	public ListenerData listener;

	/**
	 * Panning between left and right speaker (float between -1.0 and 1.0).
	 */
	private float pan = 0.0f;

	/**
	 * Constructor: Creates a new source using the specified parameters.
	 * 
	 * @param listener
	 *            Handle to information about the listener.
	 * @param priority
	 *            Setting this to true will prevent other sounds from overriding
	 *            this one.
	 * @param toStream
	 *            Setting this to true will create a streaming source.
	 * @param toLoop
	 *            Should this source loop, or play only once.
	 * @param sourcename
	 *            A unique identifier for this source. Two sources may not use
	 *            the same sourcename.
	 * @param filenameURL
	 *            Filename/URL of the sound file to play at this source.
	 * @param soundBuffer
	 *            Sound buffer to use if creating a new normal source.
	 * @param x
	 *            X position for this source.
	 * @param y
	 *            Y position for this source.
	 * @param z
	 *            Z position for this source.
	 * @param attModel
	 *            Attenuation model to use.
	 * @param distOrRoll
	 *            Either the fading distance or rolloff factor, depending on the
	 *            value of 'att'.
	 * @param temporary
	 *            Whether or not to remove this source after it finishes
	 *            playing.
	 */
	public SourceJavaSound(ListenerData listener, boolean priority,
			boolean toStream, boolean toLoop, String sourcename,
			FilenameURL filenameURL, SoundBuffer soundBuffer, float x, float y,
			float z, int attModel, float distOrRoll, boolean temporary) {
		super(priority, toStream, toLoop, sourcename, filenameURL, soundBuffer,
				x, y, z, attModel, distOrRoll, temporary);
		libraryType = LibraryJavaSound.class;

		// point handle to the listener information:
		this.listener = listener;
		positionChanged();
	}

	/**
	 * Constructor: Creates a new source matching the specified source.
	 * 
	 * @param listener
	 *            Handle to information about the listener.
	 * @param old
	 *            Source to copy information from.
	 * @param soundBuffer
	 *            Sound buffer to use if creating a new normal source.
	 */
	public SourceJavaSound(ListenerData listener, Source old,
			SoundBuffer soundBuffer) {
		super(old, soundBuffer);
		libraryType = LibraryJavaSound.class;

		// point handle to the listener information:
		this.listener = listener;
		positionChanged();
	}

	/**
	 * Constructor: Creates a new streaming source that will be directly fed
	 * with raw audio data.
	 * 
	 * @param listener
	 *            Handle to information about the listener.
	 * @param audioFormat
	 *            Format that the data will be in.
	 * @param priority
	 *            Setting this to true will prevent other sounds from overriding
	 *            this one.
	 * @param sourcename
	 *            A unique identifier for this source. Two sources may not use
	 *            the same sourcename.
	 * @param x
	 *            X position for this source.
	 * @param y
	 *            Y position for this source.
	 * @param z
	 *            Z position for this source.
	 * @param attModel
	 *            Attenuation model to use.
	 * @param distOrRoll
	 *            Either the fading distance or rolloff factor, depending on the
	 *            value of 'att'.
	 */
	public SourceJavaSound(ListenerData listener, AudioFormat audioFormat,
			boolean priority, String sourcename, float x, float y, float z,
			int attModel, float distOrRoll) {
		super(audioFormat, priority, sourcename, x, y, z, attModel, distOrRoll);
		libraryType = LibraryJavaSound.class;

		// point handle to the listener information:
		this.listener = listener;
		positionChanged();
	}

	/**
	 * Shuts the source down and removes references to all instantiated objects.
	 */
	@Override
	public void cleanup() {
		super.cleanup();
	}

	/**
	 * Changes the peripheral information about the source using the specified
	 * parameters.
	 * 
	 * @param priority
	 *            Setting this to true will prevent other sounds from overriding
	 *            this one.
	 * @param toStream
	 *            Setting this to true will create a streaming source.
	 * @param toLoop
	 *            Should this source loop, or play only once.
	 * @param sourcename
	 *            A unique identifier for this source. Two sources may not use
	 *            the same sourcename.
	 * @param filenameURL
	 *            Filename/URL of the sound file to play at this source.
	 * @param soundBuffer
	 *            Sound buffer to use if creating a new normal source.
	 * @param x
	 *            X position for this source.
	 * @param y
	 *            Y position for this source.
	 * @param z
	 *            Z position for this source.
	 * @param attModel
	 *            Attenuation model to use.
	 * @param distOrRoll
	 *            Either the fading distance or rolloff factor, depending on the
	 *            value of 'att'.
	 * @param temporary
	 *            Whether or not to remove this source after it finishes
	 *            playing.
	 */
	@Override
	public void changeSource(boolean priority, boolean toStream,
			boolean toLoop, String sourcename, FilenameURL filenameURL,
			SoundBuffer soundBuffer, float x, float y, float z, int attModel,
			float distOrRoll, boolean temporary) {
		super.changeSource(priority, toStream, toLoop, sourcename, filenameURL,
				soundBuffer, x, y, z, attModel, distOrRoll, temporary);
		if (channelJavaSound != null)
			channelJavaSound.setLooping(toLoop);
		positionChanged();
	}

	/**
	 * Called every time the listener's position or orientation changes.
	 */
	@Override
	public void listenerMoved() {
		positionChanged();
	}

	/**
	 * Sets this source's velocity, for use in Doppler effect.
	 * 
	 * @param x
	 *            Velocity along world x-axis.
	 * @param y
	 *            Velocity along world y-axis.
	 * @param z
	 *            Velocity along world z-axis.
	 */
	@Override
	public void setVelocity(float x, float y, float z) {
		super.setVelocity(x, y, z);
		positionChanged();
	}

	/**
	 * Moves the source to the specified position.
	 * 
	 * @param x
	 *            X coordinate to move to.
	 * @param y
	 *            Y coordinate to move to.
	 * @param z
	 *            Z coordinate to move to.
	 */
	@Override
	public void setPosition(float x, float y, float z) {
		super.setPosition(x, y, z);
		positionChanged();
	}

	/**
	 * Updates the pan and gain.
	 */
	@Override
	public void positionChanged() {
		calculateGain();
		calculatePan();
		calculatePitch();
	}

	/**
	 * Manually sets this source's pitch.
	 * 
	 * @param value
	 *            A float value ( 0.5f - 2.0f ).
	 */
	@Override
	public void setPitch(float value) {
		super.setPitch(value);
		calculatePitch();
	}

	/**
	 * Sets this source's attenuation model.
	 * 
	 * @param model
	 *            Attenuation model to use.
	 */
	@Override
	public void setAttenuation(int model) {
		super.setAttenuation(model);
		calculateGain();
	}

	/**
	 * Sets this source's fade distance or rolloff factor, depending on the
	 * attenuation model.
	 * 
	 * @param dr
	 *            New value for fade distance or rolloff factor.
	 */
	@Override
	public void setDistOrRoll(float dr) {
		super.setDistOrRoll(dr);
		calculateGain();
	}

	/**
	 * Plays the source on the specified channel.
	 * 
	 * @param c
	 *            Channel to play on.
	 */
	@Override
	public void play(Channel c) {
		if (!active()) {
			if (toLoop)
				toPlay = true;
			return;
		}

		if (c == null) {
			errorMessage("Unable to play source, because channel was null");
			return;
		}

		boolean newChannel = (channel != c);
		if (channel != null && channel.attachedSource != this)
			newChannel = true;

		boolean wasPaused = paused();
		boolean wasStopped = stopped();

		super.play(c);

		channelJavaSound = (ChannelJavaSound) channel;

		// Make sure the channel exists:
		// check if we are already on this channel:
		if (newChannel) {
			if (channelJavaSound != null)
				channelJavaSound.setLooping(toLoop);

			if (!toStream) {
				// This is not a streaming source, so make sure there is
				// a sound buffer loaded to play:
				if (soundBuffer == null) {
					errorMessage("No sound buffer to play");
					return;
				}

				channelJavaSound.attachBuffer(soundBuffer);
			}
		}
		positionChanged(); // set new pan and gain

		// See if we are already playing:
		if (wasStopped || !playing()) {
			if (toStream && !wasPaused) {
				preLoad = true;
			}
			channel.play();
		}
	}

	/**
	 * Queues up the initial stream-buffers for the stream.
	 * 
	 * @return False if the end of the stream was reached.
	 */
	@Override
	public boolean preLoad() {
		if (codec == null) {
			return false;
		}

		codec.initialize(filenameURL.getURL());

		LinkedList<byte[]> preLoadBuffers = new LinkedList<byte[]>();
		for (int i = 0; i < SoundSystemConfig.getNumberStreamingBuffers(); i++) {
			soundBuffer = codec.read();

			if (soundBuffer == null || soundBuffer.audioData == null)
				break;

			preLoadBuffers.add(soundBuffer.audioData);
		}
		channelJavaSound.resetStream(codec.getAudioFormat());
		positionChanged(); // set new pan and gain

		channel.preLoadBuffers(preLoadBuffers);

		preLoad = false;
		return true;
	}

	/**
	 * Calculates the gain for this source based on its attenuation model and
	 * distance from the listener.
	 */
	public void calculateGain() {
		float distX = position.x - listener.position.x;
		float distY = position.y - listener.position.y;
		float distZ = position.z - listener.position.z;

		distanceFromListener = (float) Math.sqrt(distX * distX + distY * distY
				+ distZ * distZ);

		// Calculate the source's gain using the specified attenuation model:
		switch (attModel) {
		case SoundSystemConfig.ATTENUATION_LINEAR:
			if (distanceFromListener <= 0) {
				gain = 1.0f;
			} else if (distanceFromListener >= distOrRoll) {
				gain = 0.0f;
			} else {
				gain = 1.0f - (distanceFromListener / distOrRoll);
			}
			break;
		case SoundSystemConfig.ATTENUATION_ROLLOFF:
			if (distanceFromListener <= 0) {
				gain = 1.0f;
			} else {
				float tweakFactor = 0.0005f;
				float attenuationFactor = distOrRoll * distanceFromListener
						* distanceFromListener * tweakFactor;
				// Make sure we don't do a division by zero:
				// (rolloff should NEVER be negative)
				if (attenuationFactor < 0)
					attenuationFactor = 0;

				gain = 1.0f / (1 + attenuationFactor);
			}
			break;
		default:
			gain = 1.0f;
			break;
		}
		// make sure gain is between 0 and 1:
		if (gain > 1.0f)
			gain = 1.0f;
		if (gain < 0.0f)
			gain = 0.0f;

		gain *= sourceVolume * SoundSystemConfig.getMasterGain()
				* (float) Math.abs(fadeOutGain) * fadeInGain;

		// update the channel's gain:
		if (channel != null && channel.attachedSource == this
				&& channelJavaSound != null)
			channelJavaSound.setGain(gain);
	}

	/**
	 * Calculates the panning for this source based on its position in relation
	 * to the listener.
	 */
	public void calculatePan() {
		Vector3D side = listener.up.cross(listener.lookAt);
		side.normalize();
		float x = position.dot(position.subtract(listener.position), side);
		float z = position.dot(position.subtract(listener.position),
				listener.lookAt);
		side = null;
		float angle = (float) Math.atan2(x, z);
		pan = (float) -Math.sin(angle);

		if (channel != null && channel.attachedSource == this
				&& channelJavaSound != null) {
			if (attModel == SoundSystemConfig.ATTENUATION_NONE)
				channelJavaSound.setPan(0);
			else
				channelJavaSound.setPan(pan);
		}
	}

	/**
	 * Calculates the pitch for this source based on its position in relation to
	 * the listener.
	 */
	public void calculatePitch() {
		if (channel != null && channel.attachedSource == this
				&& channelJavaSound != null) {
			// If not using Doppler effect, save some calculations:
			if (SoundSystemConfig.getDopplerFactor() == 0) {
				channelJavaSound.setPitch(pitch);
			} else {
				float SS = 343.3f;

				Vector3D SV = velocity;
				Vector3D LV = listener.velocity;
				float DV = SoundSystemConfig.getDopplerVelocity();
				float DF = SoundSystemConfig.getDopplerFactor();
				Vector3D SL = listener.position.subtract(position);

				float vls = SL.dot(LV) / SL.length();
				float vss = SL.dot(SV) / SL.length();

				vss = min(vss, SS / DF);
				vls = min(vls, SS / DF);
				float newPitch = pitch * (SS * DV - DF * vls)
						/ (SS * DV - DF * vss);

				if (newPitch < 0.5f)
					newPitch = 0.5f;
				else if (newPitch > 2.0f)
					newPitch = 2.0f;

				channelJavaSound.setPitch(newPitch);
			}
		}
	}

	public float min(float a, float b) {
		if (a < b)
			return a;
		return b;
	}
}
